package de.playmee.services;

import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.AudioManager;
import android.media.MediaMetadataRetriever;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnCompletionListener;
import android.media.MediaPlayer.OnErrorListener;
import android.media.MediaPlayer.OnPreparedListener;
import android.media.RemoteControlClient;
import android.os.PowerManager;
import android.util.Log;
import android.widget.Toast;

import java.io.IOException;
import java.util.List;

import de.playmee.Channel;
import de.playmee.Playmee;
import de.playmee.R;
import de.playmee.Track;
import de.playmee.handler.ChannelHandler;
import de.playmee.handler.ChannelTrackRatingHandler;
import de.playmee.handler.PlaylistHandler;

/**
 * Service that handles media playback
 */
public class MusicService extends ExtendedService implements OnCompletionListener,
		OnPreparedListener, OnErrorListener, MusicFocusable {

	private final static String TAG = "MusicService";

	// intent actions that can be handled by this service
	// defined by the <action> tags in the <intent-filters> tag in the AndroidManifest.xml
	public static final String ACTION_TOGGLE_PLAYBACK = "de.playmee.action.TOGGLE_PLAYBACK";
	public static final String ACTION_PLAY = "de.playmee.action.PLAY";
	public static final String ACTION_PAUSE = "de.playmee.action.PAUSE";
	public static final String ACTION_STOP = "de.playmee.action.STOP";
	public static final String ACTION_SKIP = "de.playmee.action.SKIP";
	public static final String ACTION_REWIND = "de.playmee.action.REWIND";
	public static final String ACTION_PLAY_POS = "de.playmee.action.PLAY_POS";

	// volume when we lose audio focus
	// reduce volume instead of stopping playback
	public static final float DUCK_VOLUME = 0.1f;

	// the media player
	private MediaPlayer player = null;

	// AudioFocusHelper (available in SDK 8 and higher, null if it's not available)
	private AudioFocusHelper audioFocusHelper = null;

	// state of the service
	private enum State {
		Stopped,
		Preparing,
		Playing,
		Paused
	};
	
	private State state = State.Stopped;

	// reason why the media player is paused
	enum PauseReason {
		UserRequest,
		FocusLoss,
	};
	
	PauseReason pauseReason = PauseReason.UserRequest;

	// state of audio focus
	enum AudioFocus {
		NoFocusNoDuck,
		NoFocusCanDuck,
		Focused
	}

	private AudioFocus audioFocus = AudioFocus.NoFocusNoDuck;

	// the track we are currently playing
	private Track currentTrack;

	// notification
	private final int NOTIFICATION_ID = 2;

	// playlist handler
	private PlaylistHandler pHandler;
	
	// channel handler
	private ChannelHandler cHandler;
	
	// channel-track-rating handler
	private ChannelTrackRatingHandler ctrHandler;
	private final int CHANNEL_TRACK_RATING_SKIP = 1000 * 2; // 2s
	private final int CHANNEL_TRACK_RATING_BORDER_BEGIN = 1000 * 30; // 30s
	private final double CHANNEL_TRACK_RATING_BORDER_END = 0.1; // 10%

	// RemoteControlClient for using remote control APIs (available in SDK 14 and higher)
	private RemoteControlClientCompat remoteControlClientCompat;

	// dummy album art
	private Bitmap dummyAlbumArt;

	// the MusicIntentReceiver for use with media button and remote control APIs
	private ComponentName mediaButtonReceiverComponent;

	// AudioManager to provide access to volume and ringer mode control
	private AudioManager audioManager;
	
	// list with all listeners we have to notify when the current track/channel changed
	private List<DataChangeListener> dataChangeListeners;

	/**
	 * called when the service is first created
	 */
	@Override
	public void onCreate() {
		Log.i(TAG, "Creating the MusicService...");

		audioManager = (AudioManager) getSystemService(AUDIO_SERVICE);

		Playmee application = (Playmee) getApplication();
		pHandler = application.getPlaylistHandler();
		cHandler = application.getChannelHandler();
		ctrHandler = application.getChannelTrackRatingHandler();
		dataChangeListeners = application.getDataChangeListeners();
		
		if (android.os.Build.VERSION.SDK_INT >= 8) {
			audioFocusHelper = new AudioFocusHelper(getApplicationContext(), this);
		} else {
			// no focus feature, so we always "have" audio focus
			audioFocus = AudioFocus.Focused;
		}

		dummyAlbumArt = BitmapFactory.decodeResource(getResources(), Track.DUMMY_ALBUM_ART);

		mediaButtonReceiverComponent = new ComponentName(this, MusicIntentReceiver.class);
	}

	/**
	 * called by the system every time a client explicitly starts the service
	 */
	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
		currentTrack = pHandler.getCurrentTrack();
		String action = intent.getAction();
		if (action.equals(ACTION_TOGGLE_PLAYBACK))
			processTogglePlaybackRequest();
		else if (action.equals(ACTION_PLAY))
			processPlayRequest();
		else if (action.equals(ACTION_PAUSE))
			processPauseRequest();
		else if (action.equals(ACTION_SKIP))
			processSkipRequest();
		else if (action.equals(ACTION_STOP))
			processStopRequest();
		else if (action.equals(ACTION_REWIND))
			processRewindRequest();
		else if (action.equals(ACTION_PLAY_POS))
			processPlayPositionRequest(intent.getIntExtra("pos", -1));

		// if service gets killed, after returning from here, do not restart
		return START_NOT_STICKY;
	}

	/**
	 * toggle playback request
	 */
	private void processTogglePlaybackRequest() {
		if (state == State.Paused || state == State.Stopped) {
			processPlayRequest();
		} else {
			processPauseRequest();
		}
	}

	/**
	 * play request
	 */
	private void processPlayRequest() {
		tryToGetAudioFocus();

		if (state == State.Stopped) {
			// start playing the next song
			if (currentTrack == null) {
				playNextSong();
			} else {
				playSong(pHandler.getCurrentTrack());
			}
		} else if (state == State.Paused) {
			// continue playback
			state = State.Playing;
			setUpAsForeground(NOTIFICATION_ID, currentTrack.getTitle() + " (playing)",
					R.drawable.ic_stat_playing);
			initAndStartMediaPlayer();
		}
		
		notifyRemoteControls(RemoteControlClient.PLAYSTATE_PLAYING);
	}
	
	/**
	 * play position request
	 */
	private void processPlayPositionRequest(int pos) {	
		if (pos != -1) {
			tryToGetAudioFocus();
			// TODO check if song is already playing
			playSong(pHandler.getTrackAtPosition(pos));
			notifyRemoteControls(RemoteControlClient.PLAYSTATE_PLAYING);
		}
	}

	/**
	 * pause request
	 */
	private void processPauseRequest() {
		if (state == State.Playing) {
			state = State.Paused;
			player.pause();
			relaxResources(false); // do not give up audio focus
		}
		
		notifyRemoteControls(RemoteControlClient.PLAYSTATE_PAUSED);
	}

	/**
	 * rewind request
	 */
	private void processRewindRequest() {
		if (state == State.Playing || state == State.Paused)
			player.seekTo(0);
	}

	/**
	 * skip request
	 */
	private void processSkipRequest() {
		if (state == State.Playing || state == State.Paused) {
			tryToGetAudioFocus();
			playNextSong();
		}
	}

	/**
	 * stop request
	 */
	private void processStopRequest() {
		processStopRequest(false);
	}

	/**
	 * stop request
	 * 
	 * @param force
	 */
	private void processStopRequest(boolean force) {
		if (state == State.Playing || state == State.Paused || force) {
			state = State.Stopped;
			
			// release resources
			relaxResources(true);
			giveUpAudioFocus();
			
			notifyRemoteControls(RemoteControlClient.PLAYSTATE_STOPPED);
			
			// stop the service
			stopSelf();
		}
	}
	
	/**
	 * notifies the remote controls that the playback state has changed
	 * 
	 * @param playbackState
	 */
	private void notifyRemoteControls(int playbackState) {
		if (remoteControlClientCompat != null) {
			remoteControlClientCompat.setPlaybackState(playbackState);
		}
	}

	/**
	 * releases resources used by the service
	 * 
	 * @param releaseMediaPlayer
	 */
	private void relaxResources(boolean releaseMediaPlayer) {
		// stop being a foreground service
		stopForeground(true);

		// stop and release the media player
		if (releaseMediaPlayer && player != null) {
			player.reset();
			player.release();
			player = null;
		}
	}

	/**
	 * initializes the MediaPlayer and starts (or restarts) it
	 */
	private void initAndStartMediaPlayer() {
		if (audioFocus == AudioFocus.NoFocusNoDuck) {
			// pause playback even if state is State.Playing
			// stay in the playing state and resume playback once we get the focus back
			if (player.isPlaying())
				player.pause();
			return;
		} else if (audioFocus == AudioFocus.NoFocusCanDuck) {
			// reduce volume
			player.setVolume(DUCK_VOLUME, DUCK_VOLUME);
		} else {
			// increase volume
			player.setVolume(1.0f, 1.0f);
		}

		if (!player.isPlaying())
			player.start();
	}

	/**
	 * tries to get the audio focus
	 */
	private void tryToGetAudioFocus() {
		if (audioFocus != AudioFocus.Focused && audioFocusHelper != null
				&& audioFocusHelper.requestFocus()) {
			audioFocus = AudioFocus.Focused;
		}
	}
	
	/**
	 * gives up the audio focus
	 */
	private void giveUpAudioFocus() {
		if (audioFocus == AudioFocus.Focused && audioFocusHelper != null
				&& audioFocusHelper.abandonFocus()) {
			audioFocus = AudioFocus.NoFocusNoDuck;
		}
	}
	
	/**
	 * creates the media player, or resets the existing one
	 */
	private void createMediaPlayerIfNeeded() {
		if (player == null) {
			player = new MediaPlayer();

			// acquire wake-lock
			// otherwise the cpu might go to sleep while the song is playing
			// this would stop playback
			player.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);

			// media player should notify us
			// when it's ready preparing and when it's done playing
			player.setOnPreparedListener(this);
			player.setOnCompletionListener(this);
			player.setOnErrorListener(this);
		} else
			player.reset();
	}
	
	/**
	 * starts playing the next song
	 */
	private void playNextSong() {
		playSong(pHandler.getNextTrack());
	}
	
	/**
	 * starts playing the song
	 * 
	 * @param track
	 */
	private void playSong(Track track) {
		state = State.Stopped;
		
		relaxResources(false); // release everything except MediaPlayer
		
		// update channel-track-rating of the old track
		updateChannelTrackRating(currentTrack);
		
		// asume new track
		currentTrack = track;
		
		// no next track available in the playlist
		if (currentTrack == null) {
			String msg = "No channel selected or no available music to play."
					+ " Place some music on your external storage device"
					+ " (e.g. your SD card), select a channel and try again.";
			Toast.makeText(this, msg, Toast.LENGTH_LONG).show();
			processStopRequest(true); // stop everything
			return;
		}

		try {
			// set the source of the media player to a content URI
			createMediaPlayerIfNeeded();
			player.setAudioStreamType(AudioManager.STREAM_MUSIC);
			player.setDataSource(getApplicationContext(), currentTrack.getURI());

			state = State.Preparing;
			setUpAsForeground(NOTIFICATION_ID, currentTrack.getTitle() + " (loading)",
					R.drawable.ic_stat_playing);

			// use the media button APIs (if available) to register ourselves for media button events
			MediaButtonHelper.registerMediaButtonEventReceiverCompat(audioManager, mediaButtonReceiverComponent);

			// use the remote control APIs (if available) to set the playback state
			if (remoteControlClientCompat == null) {
				Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
				intent.setComponent(mediaButtonReceiverComponent);
				remoteControlClientCompat = new RemoteControlClientCompat(PendingIntent.getBroadcast(this, 0, intent, 0));
				RemoteControlHelper.registerRemoteControlClient(audioManager, remoteControlClientCompat);
			}

			remoteControlClientCompat.setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING);

			remoteControlClientCompat.setTransportControlFlags(RemoteControlClient.FLAG_KEY_MEDIA_PLAY
					| RemoteControlClient.FLAG_KEY_MEDIA_PAUSE
					| RemoteControlClient.FLAG_KEY_MEDIA_NEXT
					| RemoteControlClient.FLAG_KEY_MEDIA_STOP);
			
			// update the remote controls
			remoteControlClientCompat
					.editMetadata(true)
					.putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, currentTrack.getArtist())
					.putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, currentTrack.getAlbum())
					.putString(MediaMetadataRetriever.METADATA_KEY_TITLE, currentTrack.getTitle())
					.putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, currentTrack.getDuration())
					// TODO fetch real item artwork
					.putBitmap(RemoteControlClientCompat.MetadataEditorCompat.METADATA_KEY_ARTWORK, dummyAlbumArt).apply();

			// start preparing the media player in the background
			// calls 'onPrepared' when media player is done preparing
			player.prepareAsync();
		}
        catch (IOException ex) {
            Log.e("MusicService", "IOException playing next song: " + ex.getMessage());
            ex.printStackTrace();
        }
	}
	
	/**
	 * updates the channel-track-rating of a given track
	 * 
	 * @param track
	 */
	private void updateChannelTrackRating(Track track) {
		if (player != null) {
			int playedTime = player.getCurrentPosition();
			int remainingTime = player.getDuration() - player.getCurrentPosition();
			int outroTime = (int) (((double)player.getDuration()) * CHANNEL_TRACK_RATING_BORDER_END);
			Log.i(TAG, "Song was played " + (((double)playedTime) / 1000) + " seconds.");
			
			long channelId = pHandler.getCurrentPlaylist().getChannelId();
			Channel channel = cHandler.loadChannel(channelId);
			
			if (playedTime > CHANNEL_TRACK_RATING_SKIP
					&& playedTime < CHANNEL_TRACK_RATING_BORDER_BEGIN) {
				// track was skipped early
				Log.i(TAG, "Reducing channel-track-rating...");
				ctrHandler.updateChannelTrackRating(channel, track, -1);
			}
			else if (playedTime > CHANNEL_TRACK_RATING_SKIP
					&& remainingTime < outroTime) {
				// track was played until the end
				Log.i(TAG, "Increasing channel-track-rating...");
				ctrHandler.updateChannelTrackRating(channel, track, 1);
			} else {
				// track was skipped somewhere in between
				Log.i(TAG, "Channel-track-rating remains unchanged.");
			}
			
			Log.i(TAG, "New rating: " + ctrHandler.getChannelTrackRating(channel, track).getRatingValue());
		}
	}
	
	/**
	 * called when media player is done preparing
	 */
	public void onPrepared(MediaPlayer player) {
		// media player is done preparing, so start playing
		state = State.Playing;
		updateNotification(NOTIFICATION_ID, currentTrack.getTitle() + " (playing)");
		
		for (DataChangeListener listener : dataChangeListeners) {
			listener.onTrackChanged(currentTrack);
		}
		
		initAndStartMediaPlayer();
	}

	/**
	 * called when media player is done playing the current song
	 */
	public void onCompletion(MediaPlayer player) {
		playNextSong();
	}

	/**
	 * called when there's an error playing media
	 */
	public boolean onError(MediaPlayer mp, int what, int extra) {
		Toast.makeText(getApplicationContext(),
				"Media player error! Resetting.", Toast.LENGTH_SHORT).show();
		Log.e(TAG, "Error: what=" + String.valueOf(what) + ", extra=" + String.valueOf(extra));

		state = State.Stopped;
		relaxResources(true);
		giveUpAudioFocus();
		return true; // true indicates we handled the error
	}

	/**
	 * called when we gain the audio focus
	 */
	public void onGainedAudioFocus() {
		Toast.makeText(getApplicationContext(),
				"gained audio focus.", Toast.LENGTH_SHORT).show();
		audioFocus = AudioFocus.Focused;

		// restart media player with new focus settings
		if (state == State.Playing)
			initAndStartMediaPlayer();
	}

	/**
	 * called when we lost the audio focus
	 */
	public void onLostAudioFocus(boolean canDuck) {
		Toast.makeText(getApplicationContext(),
				"lost audio focus." + (canDuck ? "can duck" : "no duck"),
				Toast.LENGTH_SHORT).show();
		audioFocus = canDuck ? AudioFocus.NoFocusCanDuck : AudioFocus.NoFocusNoDuck;

		// start/restart/pause media player with new focus settings
		if (player != null && player.isPlaying())
			initAndStartMediaPlayer();
	}

	/**
	 * called when the service is no longer used and is being removed
	 */
	@Override
	public void onDestroy() {
		// service is being killed, so release all resources
		state = State.Stopped;
		relaxResources(true);
		giveUpAudioFocus();
	}
	
	public interface DataChangeListener {	
		public void onTrackChanged(Track track);
		public void onChannelChanged(Channel channel);
	}
}
